package com.black.auth.authority;

import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.oauth2.common.exceptions.OAuth2Exception;
import org.springframework.security.oauth2.common.exceptions.RedirectMismatchException;
import org.springframework.security.oauth2.common.util.OAuth2Utils;
import org.springframework.security.oauth2.provider.*;
import org.springframework.security.oauth2.provider.approval.DefaultUserApprovalHandler;
import org.springframework.security.oauth2.provider.approval.UserApprovalHandler;
import org.springframework.security.oauth2.provider.code.AuthorizationCodeServices;
import org.springframework.security.oauth2.provider.endpoint.DefaultRedirectResolver;
import org.springframework.security.oauth2.provider.endpoint.RedirectResolver;
import org.springframework.security.oauth2.provider.request.DefaultOAuth2RequestValidator;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;

import java.net.URI;
import java.security.Principal;
import java.util.LinkedHashMap;
import java.util.Map;

/**
 * @author linwenfeng
 */
@Component
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class AuthorizationCodeHandler {

    private final ClientDetailsService clientDetailsService;

    private final AuthorizationCodeServices authorizationCodeServices;

    private final OAuth2RequestFactory oAuth2RequestFactory;

    private final AuthenticationManager authenticationManager;

    private final RedirectResolver redirectResolver = new DefaultRedirectResolver();

    private final UserApprovalHandler userApprovalHandler = new DefaultUserApprovalHandler();

    private final OAuth2RequestValidator oauth2RequestValidator = new DefaultOAuth2RequestValidator();


    public String authenticate(String username, String password, String clientId) throws Exception {
        UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
        authRequest.setDetails(clientId);
        Authentication authentication;
        try {
            authentication = authenticationManager.authenticate(authRequest);
        } catch (Exception exception) {
            if ("Bad credentials".equals(exception.getMessage())) {
                throw new Exception("密码不正确，请重新输入！");
            }
            throw exception;
        }
        Map<String, String> parameters = new LinkedHashMap<>();
        parameters.put("response_type", "code");
        parameters.put("client_id", clientId);
        String result;
        try {
            result = authorize(parameters, authentication);
        } catch (Exception exception) {
            throw new Exception("ClientId 不存在！");
        }
        return result;
    }

    public String authorize(Map<String, String> parameters, Principal principal) {
        AuthorizationRequest authorizationRequest = oAuth2RequestFactory.createAuthorizationRequest(parameters);
        ClientDetails client = clientDetailsService.loadClientByClientId(authorizationRequest.getClientId());
        String redirectUriParameter = authorizationRequest.getRequestParameters().get(OAuth2Utils.REDIRECT_URI);
        String resolvedRedirect = redirectResolver.resolveRedirect(redirectUriParameter, client);
        if (!StringUtils.hasText(resolvedRedirect)) {
            throw new RedirectMismatchException("A redirectUri must be either supplied or preconfigured in the ClientDetails");
        }
        authorizationRequest.setRedirectUri(resolvedRedirect);

        // We intentionally only validate the parameters requested by the client (ignoring any data that may have
        // been added to the request by the manager).
        oauth2RequestValidator.validateScope(authorizationRequest, client);

        // Some systems may allow for approval decisions to be remembered or approved by default. Check for
        // such logic here, and set the approved flag on the authorization request accordingly.
        authorizationRequest = userApprovalHandler.checkForPreApproval(authorizationRequest,
                (Authentication) principal);
        // TODO: is this call necessary?
        boolean approved = userApprovalHandler.isApproved(authorizationRequest, (Authentication) principal);
        authorizationRequest.setApproved(approved);

        return getSuccessfulRedirect(authorizationRequest, generateCode(authorizationRequest, (Authentication) principal));
    }

    private String generateCode(AuthorizationRequest authorizationRequest, Authentication authentication)
            throws AuthenticationException {
        try {
            OAuth2Authentication combinedAuth = new OAuth2Authentication(oAuth2RequestFactory.createOAuth2Request(authorizationRequest), authentication);
            return authorizationCodeServices.createAuthorizationCode(combinedAuth);
        } catch (OAuth2Exception e) {
            if (authorizationRequest.getState() != null) {
                e.addAdditionalInformation("state", authorizationRequest.getState());
            }
            throw e;
        }
    }

    private String getSuccessfulRedirect(AuthorizationRequest authorizationRequest, String authorizationCode) {
        if (authorizationCode == null) {
            throw new IllegalStateException("No authorization code found in the current request scope.");
        }
        Map<String, String> query = new LinkedHashMap<>();
        query.put("code", authorizationCode);
        String state = authorizationRequest.getState();
        if (state != null) {
            query.put("state", state);
        }
        return append(authorizationRequest.getRedirectUri(), query);
    }

    private String append(String base, Map<String, ?> query) {
        UriComponentsBuilder template = UriComponentsBuilder.newInstance();
        UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(base);
        URI redirectUri;
        try {
            // assume it's encoded to start with (if it came in over the wire)
            redirectUri = builder.build(true).toUri();
        } catch (Exception e) {
            // ... but allow client registrations to contain hard-coded non-encoded values
            redirectUri = builder.build().toUri();
            builder = UriComponentsBuilder.fromUri(redirectUri);
        }
        template.scheme(redirectUri.getScheme()).port(redirectUri.getPort()).host(redirectUri.getHost())
                .userInfo(redirectUri.getUserInfo()).path(redirectUri.getPath());
        for (String key : query.keySet()) {
            template.queryParam(key, "{" + key + "}");
        }
        template.fragment(redirectUri.getFragment());
        UriComponents encoded = template.build().expand(query).encode();
        builder.query(encoded.getQuery());
        return builder.build().toUriString();

    }
}
